package com.yahoo.astra.fl.charts
{
	import com.yahoo.astra.fl.charts.series.ISeries;
	import com.yahoo.astra.utils.NumberUtil;
	
	import fl.core.InvalidationType;
	import fl.core.UIComponent;
	
	import flash.display.Sprite;
	import flash.geom.Point;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	import flash.utils.*;
	
	/**
	 * An axis type representing a numeric range. Labels appear on major units.
	 * Ticks and grid lines may optionally appear on major and minor units.
	 * 
	 * @author Josh Tynjala
	 */
	public class NumericAxis extends Axis
	{
		
	//--------------------------------------
	//  Static Properties
	//--------------------------------------
		
		/**
		 * @private
		 */
		private static const MAX_PIXELS_BETWEEN_MINOR_VALUES:int = 30;
		
	//--------------------------------------
	//  Constructor
	//--------------------------------------
	
		/**
		 * Constructor.
		 */
		public function NumericAxis()
		{
			super(this);
		}
		
	//--------------------------------------
	//  Variables and Properties
	//--------------------------------------
	
	//-- Children
		
		/**
		 * @private
		 * The drawing canvas for the major grid lines.
		 */
		public var gridLines:Sprite;
		
		/**
		 * @private
		 * The drawing canvas for the minor grid lines.
		 */
		public var minorGridLines:Sprite;
	
		/**
		 * @private
		 * A TextField used to determine label bounds.
		 */
		private var sampleLabel:TextField = new TextField();
		
	//-- Scale
		
		/**
		 * @private
		 * Storage for the minimum value.
		 */
		private var _minimum:Number = 0;
		
		/**
		 * @private
		 * Indicates whether the minimum bound is user-defined or generated by the axis.
		 */
		private var _minimumSetByUser:Boolean = false;
		
		/**
		 * The minimum value displayed on the axis. By default, this value is generated
		 * by the axis itself. If the user defines this value, the axis will skip this
		 * automatic generation. To enable this behavior again, set this property to NaN.
		 */
		public function get minimum():Number
		{
			return this._minimum;
		}
		
		/**
		 * @private
		 */
		public function set minimum(value:Number):void
		{
			if(this._minimum != value || !this._minimumSetByUser)
			{
				this._minimum = value;
				this._minimumSetByUser = !isNaN(value);
				this.invalidate();
			}
		}
	
		/**
		 * @private
		 * Storage for the maximum value.
		 */
		private var _maximum:Number = 100;
		
		/**
		 * @private
		 * Indicates whether the maximum bound is user-defined or generated by the axis.
		 */
		private var _maximumSetByUser:Boolean = false;
		
		/**
		 * The maximum value displayed on the axis. By default, this value is generated
		 * by the axis itself. If the user defines this value, the axis will skip this
		 * automatic generation. To enable this behavior again, set this property to NaN.
		 */
		public function get maximum():Number
		{
			return this._maximum;
		}
		
		/**
		 * @private
		 */
		public function set maximum(value:Number):void
		{
			if(this._maximum != value || !this._maximumSetByUser)
			{
				this._maximum = value;
				this._maximumSetByUser = !isNaN(value);
				this.invalidate();
			}
		}
		
	//-- Units
	
		/**
		 * @private
		 * Storage for the major unit.
		 */
		private var _majorUnit:Number = 10;
		
		/**
		 * @private
		 * Indicates whether the major unit is user-defined or generated by the axis.
		 */
		private var _majorUnitSetByUser:Boolean = false;
		
		/**
		 * The major unit at which new ticks and labels are drawn. By default, this value
		 * is generated by the axis itself. If the user defines this value, the axis will
		 * skip the automatic generation. To enable this behavior again, set this property
		 * to NaN.
		 */
		public function get majorUnit():Number
		{
			return this._majorUnit;
		}
		
		/**
		 * @private
		 */
		public function set majorUnit(value:Number):void
		{
			if(this._majorUnit != value || !this._majorUnitSetByUser)
			{
				this._majorUnit = value;
				this._majorUnitSetByUser = !isNaN(value);
				this.invalidate();
			}
		}
	
		/**
		 * @private
		 * Storage for the minor unit.
		 */
		private var _minorUnit:Number = 5;
		
		/**
		 * @private
		 * Indicates whether the minor unit is user-defined or generated by the axis.
		 */
		private var _minorUnitSetByUser:Boolean = false;
		
		/**
		 * The minor unit at which new ticks are drawn. By default, this value
		 * is generated by the axis itself. If the user defines this value, the axis will
		 * skip the automatic generation. To enable this behavior again, set this property
		 * to NaN.
		 */
		public function get minorUnit():Number
		{
			return this._minorUnit;
		}
		
		/**
		 * @private
		 */
		public function set minorUnit(value:Number):void
		{
			if(this._minorUnit != value || !this._minorUnitSetByUser)
			{
				this._minorUnit = value;
				this._minorUnitSetByUser = !isNaN(value);
				this.invalidate();
			}
		}
		
		/**
		 * The value of the origin. Depends on the scale type.
		 * Note: This value may not be the "true" origin value. It may be the
		 * minimum or maximum value if the "true" origin is not visible.
		 */
		public function get origin():Number
		{
			var origin:Number = 0;
			if(this.scale == ScaleType.LOGARITHMIC)
			{
				origin = 1;
			}
			
			origin = Math.max(origin, this.minimum);
			origin = Math.min(origin, this.maximum);
			return origin;
		}
	
		/**
		 * @private
		 * Storage for the alwaysShowZero property.
		 */
		private var _alwaysShowZero:Boolean = true;
		
		/**
		 * If true, the axis will attempt to keep zero visible at all times.
		 * If both the minimum and maximum values displayed on the axis are
		 * above zero, the minimum will be reset to zero. If both minimum and
		 * maximum appear below zero, the maximum will be reset to zero. If
		 * the minimum and maximum appear at positive and negative values
		 * respectively, zero is already visible and the axis scale does not
		 * change.
		 * 
		 * <p>This property has no affect if you manually set the minimum and
		 * maximum values of the axis.</p>
		 */
		public function get alwaysShowZero():Boolean
		{
			return this._alwaysShowZero;
		}
		
		/**
		 * @private
		 */
		public function set alwaysShowZero(value:Boolean):void
		{
			if(this._alwaysShowZero != value)
			{
				this._alwaysShowZero = value;
				this.invalidate();
			}
		}
	
		/**
		 * @private
		 * Storage for the snapToUnits property.
		 */
		private var _snapToUnits:Boolean = true;
		
		/**
		 * If true, the labels, ticks, gridlines, and other objects will snap to
		 * the nearest major or minor unit. If false, their position will be based
		 * on the minimum value.
		 */
		public function get snapToUnits():Boolean
		{
			return this._snapToUnits;
		}
		
		/**
		 * @private
		 */
		public function set snapToUnits(value:Boolean):void
		{
			if(this._snapToUnits != value)
			{
				this._snapToUnits = value;
				this.invalidate();
			}
		}
		
		/**
		 * @private
		 * Storage for the scale property.
		 */
		private var _scale:String = ScaleType.LINEAR;
		
		/**
		 * The type of scaling used to display items on the axis.
		 * 
		 * @see com.yahoo.astra.fl.charts.ScaleType
		 */
		public function get scale():String
		{
			return this._scale;
		}
		
		/**
		 * @private
		 */
		public function set scale(value:String):void
		{
			if(this._scale != value)
			{
				this._scale = value;
				this.invalidate();
			}
		}

	//-- Labels
	
		/**
		 * @private
		 * Storage for the labels.
		 */
		protected var labels:Array = [];
	
		/**
		 * @private
		 * The maximum size of the labels. Comes from the width property when
		 * the axis is vertical, or the height if it is horizontal.
		 */
		private var _maxLabelSize:Number = 0;
	
	//-- Misc
	
		/**
		 * @private
		 * A multiplier used to convert from a data value to a position on the axis.
		 */
		private var _positionMultiplier:Number = 1;
		
		/**
		 * @private
		 */
		private var _dataMinimum:Number = NaN;
		
		/**
		 * @private
		 */
		private var _dataMaximum:Number = NaN;
		
	//--------------------------------------
	//  Public Methods
	//--------------------------------------
	
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#valueToLocal()
		 */
		override public function valueToLocal(data:Object):Number
		{
			if(data == null) return NaN;
			
			var position:Number = 0;
			
			if(this.scale == ScaleType.LINEAR)
			{
				position = (Number(data) - this.minimum) * this._positionMultiplier;
			}
			else
			{
				var logOfData:Number = Math.log(Number(data));
				var logOfMinimum:Number = Math.log(this.minimum);
				position = (logOfData - logOfMinimum) * this._positionMultiplier;
			}
			
			if(this.reverse && this.orientation == AxisOrientation.HORIZONTAL)
			{
				position = this._contentBounds.width - position;
			}
			else if(!this.reverse && this.orientation == AxisOrientation.VERTICAL)
			{
				position = this._contentBounds.height - position;
			}
				
			return position;
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#localToValue()
		 */
		override public function localToValue(position:Number):Object
		{
			if(this.reverse && this.orientation == AxisOrientation.HORIZONTAL)
			{
				position = this._contentBounds.width + position;
			}
			else if(!this.reverse && this.orientation == AxisOrientation.VERTICAL)
			{
				position = this._contentBounds.height + position;
			}
			
			return (position / this._positionMultiplier) + this.minimum;
		}
		
		/**
		 * @private
		 */
		override public function invalidate(property:String = InvalidationType.ALL, callLater:Boolean = true):void
		{
			//we need to make sure direct changes to this axis cause the whole chart to redraw!
			if(!inCallLaterPhase && this.parent is UIComponent) 
			{
				UIComponent(this.parent).invalidate();
			}
			super.invalidate(property, callLater);
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#updateScale()
		 */
		override public function updateScale(data:Array):void
		{
			super.updateScale(data);
			
			var seriesCount:int = data.length;
			var dataMinimum:Number = NaN;
			var dataMaximum:Number = NaN;
			for(var i:int = 0; i < seriesCount; i++)
			{
				var series:ISeries = data[i] as ISeries;
				var dataField:String = this.plotArea.axisAndSeriesToField(this, series);
				var seriesLength:int = series.length;
				for(var j:int = 0; j < seriesLength; j++)
				{
					var item:Object = series.dataProvider[j];
					if(item is Number)
					{
						var value:Number = item as Number;
					}
					else if(dataField && item.hasOwnProperty(dataField))
					{
						value = Number(item[dataField]);
					}
					
					//skip bad data
					if(isNaN(value))
					{
						continue;
					}
					
					if(isNaN(dataMinimum))
					{
						dataMinimum = value;
					}
					else
					{
						dataMinimum = Math.min(dataMinimum, value);
					}
					if(isNaN(dataMaximum))
					{
						dataMaximum = value;
					}
					else
					{
						dataMaximum = Math.max(dataMaximum, value);
					}
				}
			}
			
			if(!isNaN(dataMinimum))
			{
				this._dataMinimum = dataMinimum;
			}
			if(!isNaN(dataMaximum))
			{
				this._dataMaximum = dataMaximum;
			}
			this.resetScale();
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#updateScale()
		 */
		override public function updateBounds():void
		{
			this.refreshLabels();
			
			super.updateBounds();
			
			this.calculatePositionMultiplier();
		}
	
	//--------------------------------------
	//  Protected Methods
	//--------------------------------------
	
		/**
		 * @private
		 */
		override protected function draw():void
		{
			this.updateBounds();
			
			this.graphics.clear();
			
			this.drawAxis();
			this.drawObjectsOnMinorUnit();
			this.drawObjectsOnMajorUnit();
			
			super.draw();
		}
		
		/**
		 * If the minimum, maximum, major unit or minor unit have not been set by the user,
		 * these values must be generated by the axis. May be overridden to use custom
		 * scaling algorithms.
		 */
		protected function resetScale():void
		{
			if(isNaN(this._dataMaximum) || isNaN(this._dataMinimum))
			{
				//BAD DATA.
				return;
			}
			
			if(!this._minimumSetByUser)
			{
				this._minimum = this._dataMinimum;
			}
			if(!this._maximumSetByUser)
			{
				this._maximum = this._dataMaximum;
			}
			
			//swap min and max if needed
			if(this._minimum > this._maximum)
			{
				var temp:Number = this._minimum;
				this._minimum = this._maximum;
				this._maximum = temp;
				
				//be sure to swap these flags too!
				var temp2:Boolean = this._minimumSetByUser;
				this._minimumSetByUser = this._maximumSetByUser;
				this._maximumSetByUser = temp2;
			}
			
			//if we're pinned to zero, make sure zero is in the range
			if(this.alwaysShowZero)
			{
				if(!this._minimumSetByUser && this._minimum > 0 && this._maximum > 0)
				{
					this._minimum = 0;
				}
				else if(!this._maximumSetByUser && this._minimum < 0 && this._maximum < 0)
				{
					this._maximum = 0;
				}
			}
			
			if(!this._maximumSetByUser && this._minimum == this._maximum)
			{
				this._maximum + this._minimum + 1;
			}
			
			var lengthOfAxis:Number = this.width;
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				lengthOfAxis = this.height;
			}
			
			var sampleLabel:TextField = new TextField();
			sampleLabel.defaultTextFormat = this.getStyleValue("textFormat") as TextFormat;
			sampleLabel.text = "999.99%";
			var labelHeight:Number = sampleLabel.height;
			
			var calculatedMajorUnit:Number = this._majorUnit;
			if(!this._majorUnitSetByUser)
			{
				var range:Number = this.maximum - this.minimum;
				range = NumberUtil.roundToPrecision(range, 10);
				if(range != 0)
				{
					var rangeAsString:String = range.toString();
					var degree:Number = 1;
					
					var decimalIndex:int = rangeAsString.indexOf(".");
					if(range > 1 || range < -1)
					{
						//remove the fractional component
						if(decimalIndex >= 0) rangeAsString = rangeAsString.substr(0, decimalIndex);
						degree = rangeAsString.length - 1;
					}
					else //fractional value
					{
						rangeAsString = rangeAsString.substr(decimalIndex + 1);
						
						//find the first non-zero
						var fractionalLength:int = rangeAsString.length;
						degree = -1;
						for(var i:int = 0; i < fractionalLength; i++)
						{
							var letter:String = rangeAsString.charAt(i);
							if(letter != "0") break;
							degree--;
						}
					}
					//TODO: Make unit calculation a swappable algorithm
					var detectedPowerOfTen:Number = Math.pow(10, degree);
					calculatedMajorUnit = detectedPowerOfTen;
					
					//make sure there is enough room for the labels and that the major unit
					//length hasn't gotten bigger than the axis.
					var majorUnitSpacing:Number = lengthOfAxis / (range / calculatedMajorUnit);
					var labelBounds:Point = this.calculateMaximumLabelBounds(calculatedMajorUnit);
					var minimumSpacing:Number;
					if(this.orientation == AxisOrientation.VERTICAL)
					{
						minimumSpacing = Math.max(50, labelBounds.y);
					}
					else minimumSpacing = Math.max(50, labelBounds.x);
					
					while(majorUnitSpacing < minimumSpacing && majorUnitSpacing < lengthOfAxis)
					{
						calculatedMajorUnit += detectedPowerOfTen;
						calculatedMajorUnit = NumberUtil.roundToPrecision(calculatedMajorUnit, 10);
						
						majorUnitSpacing = lengthOfAxis / ((this.maximum - this.minimum) / calculatedMajorUnit);
						labelBounds = this.calculateMaximumLabelBounds(calculatedMajorUnit);
						if(this.orientation == AxisOrientation.VERTICAL)
						{
							minimumSpacing = Math.max(50, labelBounds.y);
						}
						else minimumSpacing = Math.max(50, labelBounds.x);
					}
						
					//if the spacing between major units
					if(majorUnitSpacing > minimumSpacing * 2)
					{
						calculatedMajorUnit -= (calculatedMajorUnit / 2);
						calculatedMajorUnit = NumberUtil.roundToPrecision(calculatedMajorUnit, 10);
					}
				}
			}
			//don't change the maximum if the user set it or it is pinned to zero
			if(!this._maximumSetByUser && !(this.alwaysShowZero && this._maximum == 0))
			{
				var oldMaximum:Number = this._maximum;
				this._maximum = NumberUtil.roundUpToNearest(this._maximum, calculatedMajorUnit);
				if(this._maximum == oldMaximum) this._maximum += calculatedMajorUnit;
			}
			
			//don't change the minimum if the user set it or it is pinned to zero
			if(!this._minimumSetByUser && !(this.alwaysShowZero && this._minimum == 0))
			{
				var oldMinimum:Number = this._minimum;
				this._minimum = NumberUtil.roundDownToNearest(this._minimum, calculatedMajorUnit);
				if(this._minimum == oldMinimum) this._minimum -= calculatedMajorUnit;
			}
				
			//TODO: Determine if there's a better way to handle this...
			if(!this._minimumSetByUser && this.scale == ScaleType.LOGARITHMIC && this._minimum <= 0)
			{
				//use the data minimum or 1 if the data goes outside the allowed range
				if(this._dataMinimum > 0 && this._dataMinimum < 1)
				{
					this._minimum = this._dataMinimum;
				}
				else this._minimum = 1;
			}
			
			if(!this._majorUnitSetByUser)
			{
				this._majorUnit = calculatedMajorUnit;
			}
			majorUnitSpacing = this._majorUnit == 0 ? 0 : lengthOfAxis / ((this._maximum - this._minimum) / this._majorUnit);
			
			if(!this._maximumSetByUser && this._minimum == this._maximum)
			{
				this._maximum = this._minimum + 1;
				//let's make a nice major unit for our difference of one
				if(!this._majorUnitSetByUser)
				{
					this._majorUnit = 0.5;
				}
			}
			
			if(!this._minorUnitSetByUser)
			{
				var maximumMinorValueCount:int = int(majorUnitSpacing / MAX_PIXELS_BETWEEN_MINOR_VALUES);
				if(maximumMinorValueCount != 0)
				{
					var smallestMinorUnit:Number = this._majorUnit / maximumMinorValueCount;
					
					this._minorUnit = smallestMinorUnit;
					do
					{
						var lastMinorUnit:Number = this._minorUnit;
						this._minorUnit = this.niceNumber(this._minorUnit);
						
						if(lastMinorUnit == this._minorUnit) break;
						if(this._minorUnit >= this._majorUnit)
						{
							this._minorUnit = this._majorUnit;
							break;
						}
					}
					//make sure we have a good division (not 25 and 10, for example)
					while(this._minorUnit != 0 && this._majorUnit % this._minorUnit != 0)
				}
			}
			
			//even if they are manually set by the user, check all values for possible floating point errors.
			//we don't want extra labels or anything like that!
			this._minimum = NumberUtil.roundToPrecision(this._minimum, 10);
			this._maximum = NumberUtil.roundToPrecision(this._maximum, 10);
			this._majorUnit = NumberUtil.roundToPrecision(this._majorUnit, 10);
			this._minorUnit = NumberUtil.roundToPrecision(this._minorUnit, 10);
		}

		/**
		 * @private
		 */
		protected function niceNumber(value:Number):Number
		{
			if(value == 0) return 0;
			
			var count:int = 0;
			while(value > 10e-8)
			{
				value /= 10;
				count++;
			}

		    if(value >= 4.0e-8)
		    {
		        value = 5.0e-8;
		    }
		    else if(value >= 2.0e-8)
		    {
		        value = 2.5e-8;
		    }
		    else if(value >= 1.0e-8)
		    {
		        value = 2.0e-8;
		    }
		    else
		    {
		        value = 1.0e-8;
		    }
		
			
		    for(var i:int = count; i > 0; i--)
		    {
		    	value *= 10;
		    }
		
		    return value;
		}
		
		/**
		 * @private
		 */
		protected function calculateMaximumLabelBounds(proposedMajorUnit:Number):Point
		{
			this.sampleLabel.defaultTextFormat = this.getStyleValue("textFormat") as TextFormat;
			this.sampleLabel.autoSize = TextFieldAutoSize.LEFT;
			
			var maximumBounds:Point = new Point();
			for(var i:Number = this.minimum; i <= this.maximum; i += proposedMajorUnit)
			{
				if(this.snapToUnits && i != this.minimum)
				{
					i = NumberUtil.roundDownToNearest(i, proposedMajorUnit);
				}
				i = Math.max(i, this.minimum);
				i = Math.min(i, this.maximum);
				i = NumberUtil.roundToPrecision(i, 10);
				
				var text:String = this.valueToLabel(i);
				
				var bounds:Point = this.measureLabel(text);
				maximumBounds.x = Math.max(maximumBounds.x, bounds.x);
				maximumBounds.y = Math.max(maximumBounds.y, bounds.y);
			}
			
			return maximumBounds;
		}
		
		/**
		 * @private
		 */
		protected function measureLabel(text:String):Point
		{
			if(text == null) text = "";
			this.sampleLabel.text = text;
			return new Point(this.sampleLabel.width, this.sampleLabel.height);
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.Axis#calculateContentBounds()
		 */
		override protected function calculateContentBounds():void
		{
			super.calculateContentBounds();
			
			if(this.labels.length > 0)
			{
				var firstLabel:TextField = this.labels[0] as TextField;
				var lastLabel:TextField = this.labels[this.labels.length - 1] as TextField;
				if(this.reverse)
				{
					var temp:TextField = firstLabel;
					firstLabel = lastLabel;
					lastLabel = temp;
				}
			}
			
			var showTicks:Boolean = this.getStyleValue("showTicks") as Boolean;
			var showMinorTicks:Boolean = this.getStyleValue("showMinorTicks") as Boolean;
			var tickLength:Number = this.getStyleValue("tickLength") as Number;
			var minorTickLength:Number = this.getStyleValue("minorTickLength") as Number;
			var tickPosition:String = this.getStyleValue("tickPosition") as String;
			var minorTickPosition:String = this.getStyleValue("minorTickPosition") as String;
			var labelDistance:Number = this.getStyleValue("labelDistance") as Number;
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				var firstLabelHeight:Number = 0;
				var lastLabelHeight:Number = 0;
				if(this.labels.length > 0)
				{
					if(firstLabel.text.length > 0) firstLabelHeight = firstLabel.height;
					if(lastLabel.text.length > 0) lastLabelHeight = lastLabel.height;
				}
				
				var contentBoundsX:Number = 0;
				if(this.labels.length > 0)
				{
					contentBoundsX = this._maxLabelSize + labelDistance;
				}
				
				var tickContentBoundsX:Number = 0;
				if(showTicks)
				{
					switch(tickPosition)
					{
						case TickPosition.OUTSIDE:
						case TickPosition.CROSS:
							tickContentBoundsX = tickLength;
							break;
					}
				}
				if(showMinorTicks)
				{
					switch(minorTickPosition)
					{
						case TickPosition.OUTSIDE:
						case TickPosition.CROSS:
							tickContentBoundsX = Math.max(tickContentBoundsX, minorTickLength);
					}
				}
				contentBoundsX += tickContentBoundsX;
				this._contentBounds.x += contentBoundsX;
				this._contentBounds.width -= contentBoundsX;
				
				var contentBoundsY:Number = 0;
				if(this.labels.length > 0) contentBoundsY = firstLabelHeight / 2;
				this._contentBounds.y += contentBoundsY;
				this._contentBounds.height -=  contentBoundsY;
				
				if(this.labels.length > 0) this._contentBounds.height -= lastLabelHeight / 2
			}
			else
			{
				var firstLabelWidth:Number = 0;
				var lastLabelWidth:Number = 0;
				if(this.labels.length > 0)
				{
					if(firstLabel.text.length > 0) firstLabelWidth = firstLabel.width;
					if(lastLabel.text.length > 0) lastLabelWidth = lastLabel.width;
				}
				
				contentBoundsX = 0;
				if(this.labels.length > 0) contentBoundsX = firstLabelWidth / 2;
				this._contentBounds.x += contentBoundsX;
				this._contentBounds.width -= contentBoundsX;
				
				if(this.labels.length > 0)
				{
					this._contentBounds.width -= lastLabelWidth / 2;
					this._contentBounds.height -= (this._maxLabelSize + labelDistance);
				}
				
				var tickHeight:Number = 0;
				if(showTicks)
				{
					switch(tickPosition)
					{
						case TickPosition.OUTSIDE:
						case TickPosition.CROSS:
							tickHeight = tickLength;
							break;
					}
				}
				if(showMinorTicks)
				{
					switch(minorTickPosition)
					{
						case TickPosition.OUTSIDE:
						case TickPosition.CROSS:
							tickHeight = Math.max(tickHeight, minorTickLength);
							break;
					}
				}
				
				this._contentBounds.height -= tickHeight;
			}
		}
	
		/**
		 * Draws the axis origin line.
		 */
		protected function drawAxis():void
		{
			var min:Number = this.valueToLocal(this.minimum);
			var max:Number = this.valueToLocal(this.maximum);
			
			var axisWeight:int = this.getStyleValue("axisWeight") as int;
			var axisColor:uint = this.getStyleValue("axisColor") as uint;
			this.graphics.lineStyle(axisWeight, axisColor);
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				this.graphics.moveTo(this._contentBounds.x, this._contentBounds.y + min);
				this.graphics.lineTo(this._contentBounds.x, this._contentBounds.y + max);
			}
			else //horizontal
			{
				this.graphics.moveTo(this._contentBounds.x + min, this._contentBounds.y + this._contentBounds.height);
				this.graphics.lineTo(this._contentBounds.x + max, this._contentBounds.y + this._contentBounds.height);
			}
		}
		
		/**
		 * Draws labels, grid lines, and ticks at the major unit positions.
		 */
		protected function drawObjectsOnMajorUnit():void
		{
			if(this.gridLines) this.gridLines.graphics.clear();
			if(this.majorUnit <= 0) return;
			
			var lastVisibleLabel:TextField;
			var labelCount:int = 0;
			var displayedMaximum:Boolean = false;
			var value:Number = this.minimum;
			
			//get the required styles
			var showLabels:Boolean = this.getStyleValue("showLabels") as Boolean;
			var labelDistance:Number = this.getStyleValue("labelDistance") as Number;
			
			var showGridLines:Boolean = this.getStyleValue("showGridLines") as Boolean;
			var gridLineWeight:int = this.getStyleValue("gridLineWeight") as int;
			var gridLineColor:uint = this.getStyleValue("gridLineColor") as uint;
			
			var showTicks:Boolean = this.getStyleValue("showTicks") as Boolean;
			var tickPosition:String = this.getStyleValue("tickPosition") as String;
			var tickLength:Number = this.getStyleValue("tickLength") as Number;
			var tickWeight:int = this.getStyleValue("tickWeight") as int;
			var tickColor:uint = this.getStyleValue("tickColor") as uint;
					
			while(value <= this.maximum)
			{
				var label:TextField = this.labels[labelCount];
				
				//because Flash UIComponents round the position to the nearest pixel, we need to do the same.
				var position:Number = Math.round(this.valueToLocal(value));
				
				//draw the grid lines
				if(this.gridLines && showGridLines)
				{
					this.gridLines.graphics.lineStyle(gridLineWeight, gridLineColor);
					//the gridlines are positioned at (contentBounds.x, contentBounds.y)
					if(this.orientation == AxisOrientation.VERTICAL)
					{
						this.gridLines.graphics.moveTo(0, position);
						this.gridLines.graphics.lineTo(this.contentBounds.width, position);
					}
					else
					{
						this.gridLines.graphics.moveTo(position, 0);
						this.gridLines.graphics.lineTo(position, this.contentBounds.height);
					}
				}
				
				//position the label
				if(this.orientation == AxisOrientation.VERTICAL)
				{
					position += this._contentBounds.y;
					if(showLabels)
					{
						label.x = this._contentBounds.x - label.width - labelDistance;
						label.y = position - label.height / 2;
					}
				}
				else
				{
					position += this._contentBounds.x;
					if(showLabels)
					{
						label.x = position - label.width / 2;
						label.y = this._contentBounds.height + labelDistance;
					}
				}
				
				//draw the ticks
				if(showTicks)
				{
					this.graphics.lineStyle(tickWeight, tickColor);
					switch(tickPosition)
					{
						case TickPosition.OUTSIDE:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x - tickLength, position);
								this.graphics.lineTo(this._contentBounds.x, position);
								if(showLabels) label.x -= tickLength;
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height + tickLength);
								if(showLabels) label.y += tickLength;
							}
							break;
						case TickPosition.INSIDE:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x, position);
								this.graphics.lineTo(this._contentBounds.x + tickLength, position);
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height - tickLength);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height);
							}
							break;
						default: //CROSS
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x - tickLength, position);
								this.graphics.lineTo(this._contentBounds.x + tickLength, position);
								if(showLabels) label.x -= tickLength;
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height - tickLength);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height + tickLength);
								if(showLabels) label.y += tickLength;
							}
					}
				}
				
				//hide overlap if needed
				if(showLabels && this.hideOverlappingLabels)
				{
					label.visible = true;
					if(lastVisibleLabel)
					{
						if(this.orientation == AxisOrientation.VERTICAL)
						{
							if(!this.reverse && label.y + label.height > lastVisibleLabel.y ||
								this.reverse && lastVisibleLabel.y + lastVisibleLabel.height > label.y)
							{
								//keep the last label, and hide the one before it (unless that's the minimum)
								if(displayedMaximum && this.labels.indexOf(lastVisibleLabel) != 0)
								{
									lastVisibleLabel.visible = false;
								}
								//don't hide the last label
								else if(this.labels.indexOf(label) != this.labels.length - 1)
								{
									label.visible = false;
								}	
							}
						}
						else
						{
							if(this.reverse && label.x + label.width > lastVisibleLabel.x ||
								!this.reverse && lastVisibleLabel.x + lastVisibleLabel.width > label.x)
							{
								//keep the last label, and hide the one before it (unless that's the minimum)
								if(displayedMaximum && this.labels.indexOf(lastVisibleLabel) != 0)
								{
									lastVisibleLabel.visible = false;
								}
								//don't hide the last label
								else if(this.labels.indexOf(label) != this.labels.length - 1)
								{
									label.visible = false;
								}
							}
						}
					}
					if(label.visible) lastVisibleLabel = label;
				}
				//TODO: Consider making a pass back from the last label to see if any other labels could be shown again
				
				//if the maximum has been displayed, we're done!
				if(displayedMaximum) break;
				
				//a bad major unit will get us stuck in an infinite loop
				if(this.majorUnit <= 0)
				{
					value = this.maximum;
				}
				else
				{
					value += this.majorUnit;
					if(this.snapToUnits) value = NumberUtil.roundDownToNearest(value, this.majorUnit);
					value = Math.min(value, this.maximum); //don't go over the max!
				}
				displayedMaximum = NumberUtil.fuzzyEquals(value, this.maximum);
				
				//make sure we don't use too many labels
				labelCount++;
			}
				
		}
		
		/**
		 * Draws grid lines and ticks at the minor unit positions.
		 */
		protected function drawObjectsOnMinorUnit():void
		{
			if(this.minorGridLines) this.minorGridLines.graphics.clear();
			if(this.minorUnit <= 0) return;
			
			var showTicks:Boolean = this.getStyleValue("showTicks");
			var showMinorGridLines:Boolean = this.getStyleValue("showMinorGridLines") as Boolean;
			var minorGridLineWeight:int = this.getStyleValue("minorGridLineWeight") as int;
			var minorGridLineColor:uint = this.getStyleValue("minorGridLineColor") as uint;
			var showMinorTicks:Boolean = this.getStyleValue("showMinorTicks") as Boolean;
			var minorTickPosition:String = this.getStyleValue("minorTickPosition") as String;
			var minorTickLength:Number = this.getStyleValue("minorTickLength") as Number;
			
			var displayedMaximum:Boolean = false;
			var value:Number = this.minimum;
			var skipMajorUnits:Boolean = showTicks && this.majorUnit > 0;
			
			while(value <= this.maximum)
			{
				//if major ticks are drawn, don't draw minor ticks on major units
				//sometimes Flash doesn't pick the correct position and you can tell that the ticks overlap.
				//use fuzzyEquals to work around floating point errors
				while(skipMajorUnits && ((!this.snapToUnits && NumberUtil.fuzzyEquals((value - this.minimum) % this.majorUnit, 0)) || (this.snapToUnits && NumberUtil.fuzzyEquals((value) % this.majorUnit, 0)))
					|| NumberUtil.fuzzyEquals(value, this.maximum)
					|| NumberUtil.fuzzyEquals(value, this.minimum))
				{
					//would be "break", but we're in a nested loop.
					if(displayedMaximum) return;
					
					value += this.minorUnit;
					if(this.snapToUnits) value = NumberUtil.roundDownToNearest(value, this.minorUnit);
					value = Math.min(value, this.maximum); //don't go over the max!
					displayedMaximum = NumberUtil.fuzzyEquals(value, this.maximum);
				}			
				
				//because Flash UIComponents round the position to the nearest pixel, we need to do the same.
				var position:Number = Math.round(this.valueToLocal(value));
				
				//draw the grid lines
				if(this.minorGridLines && showMinorGridLines)
				{
					this.minorGridLines.graphics.lineStyle(minorGridLineWeight, minorGridLineColor);
					//the gridlines are positioned at (contentBounds.x, contentBounds.y)
					if(this.orientation == AxisOrientation.VERTICAL)
					{
						this.minorGridLines.graphics.moveTo(0, position);
						this.minorGridLines.graphics.lineTo(this.contentBounds.width, position);
					}
					else
					{
						this.minorGridLines.graphics.moveTo(position, 0);
						this.minorGridLines.graphics.lineTo(position, this._contentBounds.height);
					}
				}
				
				if(this.orientation == AxisOrientation.VERTICAL)
				{
					position += this._contentBounds.y;
				}
				else
				{
					position += this._contentBounds.x;
				}
				
				if(showMinorTicks)
				{
					var minorTickWeight:int = this.getStyleValue("minorTickWeight") as int;
					var minorTickColor:uint = this.getStyleValue("minorTickColor") as uint;
					this.graphics.lineStyle(minorTickWeight, minorTickColor);
					switch(minorTickPosition)
					{
						case TickPosition.OUTSIDE:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x - minorTickLength, position);
								this.graphics.lineTo(this._contentBounds.x, position);
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height + minorTickLength);
							}
							break;
						case TickPosition.INSIDE:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x, position);
								this.graphics.lineTo(this._contentBounds.x + minorTickLength, position);
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height - minorTickLength);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height);
							}
							break;
						default: //CROSS
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x - minorTickLength / 2, position);
								this.graphics.lineTo(this._contentBounds.x + minorTickLength / 2, position);
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height - minorTickLength / 2);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height + minorTickLength / 2);
							}
					}
				}
				
				if(displayedMaximum) break;
				
				value += this.minorUnit;
				if(this.snapToUnits) value = NumberUtil.roundDownToNearest(value, this.minorUnit);
				value = Math.min(value, this.maximum); //don't go over the max!
				displayedMaximum = NumberUtil.fuzzyEquals(value, this.maximum);
			}
				
		}
	
	//--------------------------------------
	//  Private Methods
	//--------------------------------------
	
		/**
		 * @private
		 * Update the labels by adding or removing some, setting the text, etc.
		 */
		private function refreshLabels():void
		{
			var showLabels:Boolean = this.getStyleValue("showLabels") as Boolean;
			var labelCount:int = showLabels ? this.calculateLabelCount() : 0;
			var difference:int = labelCount - this.labels.length;
			if(difference > 0)
			{
				//add new labels
				for(var i:int = 0; i < difference; i++)
				{
					var label:TextField = new TextField();
					label.selectable = false;
					label.autoSize = TextFieldAutoSize.LEFT;
					this.labels.push(label);
					this.addChild(label);
				}
			}
			else if(difference < 0)
			{
				//remove existing labels
				difference = Math.abs(difference);
				for(i = 0; i < difference; i++)
				{
					label = this.labels.pop() as TextField;
					this.removeChild(label);
				}
			}
			
			var textFormat:TextFormat = this.getStyleValue("textFormat") as TextFormat;
			for(i = 0; i < labelCount; i++)
			{
				label = this.labels[i] as TextField;
				label.defaultTextFormat = textFormat;
			}
			
			//with the labels in place, update the displayed text
			if(showLabels)
			{
				this.setLabelText();
			}
		}
		
		/**
		 * @private
		 */
		private function calculateLabelCount():int
		{
			var showLabels:Boolean = this.getStyleValue("showLabels");
			if(!showLabels) return 0;
			
			if(this.majorUnit <= 0) return 2; //min and max
			
			//it might be possible to optimize this to a simple equation. Snapping to major units makes that questionable, though.
			var labelCount:int = 1;
			var value:Number = this.minimum;
			do
			{
				value += this.majorUnit;
				if(this.snapToUnits) value = NumberUtil.roundDownToNearest(value, this.majorUnit);
				value = NumberUtil.roundToPrecision(value, 10);
				labelCount++;
			}
			while(value < this.maximum);
			
			if(value < this.maximum) labelCount++;
			
			return labelCount;
		}
		
		/**
		 * @private
		 * Sets the text for each label and determines the offets.
		 */
		private function setLabelText():void
		{
			this._maxLabelSize = 0;
			var labelCount:int = this.labels.length;
			for(var i:int = 0; i < labelCount; i++)
			{
				var label:TextField = this.labels[i] as TextField;
				var value:Number = this.minimum + i * this.majorUnit;
				if(this.snapToUnits && value != this.minimum)
				{
					value = NumberUtil.roundDownToNearest(value, this.majorUnit);
				}
				value = Math.max(value, this.minimum);
				value = Math.min(value, this.maximum);
				value = NumberUtil.roundToPrecision(value, 10);
				
				label.text = this.valueToLabel(value);
				
				if(label.text.length == 0) continue;
				
				if(this.orientation == AxisOrientation.VERTICAL)
				{
					this._maxLabelSize = Math.max(this._maxLabelSize, label.width);
				}
				else this._maxLabelSize = Math.max(this._maxLabelSize, label.height);
			}
		}
	
		/**
		 * @private
		 * Calculates the multiplier used to convert a data point to an actual position
		 * on the axis.
		 */
		private function calculatePositionMultiplier():void
		{
			var range:Number = this.maximum - this.minimum;
			if(this.scale == ScaleType.LOGARITHMIC)
			{
				range = Math.log(this.maximum) - Math.log(this.minimum);
			}
			if(range == 0)
			{
				this._positionMultiplier = 0;
				return;
			}
			
			var length:Number = this._contentBounds.width;
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				length = this._contentBounds.height;
			}
			this._positionMultiplier = length / range;
		}
		
	}
}
